﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Hive.Core
{
    public enum Tile { Land, Water, Unseen }
    public enum Entity { Ant, Enemy, Hill, Food, Dead }

    public class Map
    {
        public int Width { get; private set; }
        public int Height { get; private set; }

        //public static readonly Map Instance = new Map();
        public Tile this[Location location]
        {
            get { return this.map[location.Row, location.Col]; }
            set { this.map[location.Row, location.Col] = value; }
        }

        public Tile this[int row, int col]
        {
            get { return this.map[row, col]; }
            set { this.map[row, col] = value; }
        }

        public int TileCount
        {
            get
            {
                return Width * Height;
            }
        }

        public int TileCountUnseen
        {
            get
            {
                return Count(t => t == Tile.Unseen);
            }
        }

        public int Count(Predicate<Tile> p)
        {
            var result = 0;
            map.Iter((r, c, tile) => { if (p(tile)) result++; });
            return result;
        }

        Tile[,] map;

        public void Init(int width, int height)
        {
            Width = width;
            Height = height;

            map = new Tile[height, width];
            for (int row = 0; row < height; row++)
            {
                for (int col = 0; col < width; col++)
                {
                    map[row, col] = Tile.Unseen;
                }
            }

        }

        public void AddWater(int row, int col)
        {
            map[row, col] = Tile.Water;
        }

        /// <summary>
        /// Gets whether <paramref name="location"/> is passable or not.
        /// </summary>
        /// <param name="location">The location to check.</param>
        /// <returns><c>true</c> if the location is not water, <c>false</c> otherwise.</returns>
        /// <seealso cref="GetIsUnoccupied"/>
        public bool GetIsPassable(Location location)
        {
            return map.At(location) != Tile.Water;
        }
        /// <summary>
        /// Gets whether <paramref name="location"/> is occupied or not.
        /// </summary>
        /// <param name="location">The location to check.</param>
        /// <returns><c>true</c> if the location is passable and does not contain an ant, <c>false</c> otherwise.</returns>
        public bool GetIsUnoccupied(Location location, GameState state)
        {
            return GetIsPassable(location) && state.MyAnts.Any(a => a.Equals(location));
        }

        /// <summary>
        /// Gets the destination if an ant at <paramref name="location"/> goes in <paramref name="direction"/>, accounting for wrap around.
        /// </summary>
        /// <param name="location">The starting location.</param>
        /// <param name="direction">The direction to move.</param>
        /// <returns>The new location, accounting for wrap around.</returns>
        public Location GetDestination(Location location, Direction direction)
        {
            Location delta = Engine.Aim[direction];

            int row = (location.Row + delta.Row) % Height;
            if (row < 0) row += Height; // because the modulo of a negative number is negative

            int col = (location.Col + delta.Col) % Width;
            if (col < 0) col += Width;

            return new Location(row, col);
        }

        /// <summary>
        /// Gets the distance between <paramref name="loc1"/> and <paramref name="loc2"/>.
        /// </summary>
        /// <param name="loc1">The first location to measure with.</param>
        /// <param name="loc2">The second location to measure with.</param>
        /// <returns>The distance between <paramref name="loc1"/> and <paramref name="loc2"/></returns>
        public int StupidGetDistance(Location loc1, Location loc2)
        {
            int d_row = Math.Abs(loc1.Row - loc2.Row);
            d_row = Math.Min(d_row, Height - d_row);

            int d_col = Math.Abs(loc1.Col - loc2.Col);
            d_col = Math.Min(d_col, Width - d_col);

            return d_row + d_col;
        }

        /// <summary>
        /// Gets the closest directions to get from <paramref name="loc1"/> to <paramref name="loc2"/>.
        /// </summary>
        /// <param name="loc1">The location to start from.</param>
        /// <param name="loc2">The location to determine directions towards.</param>
        /// <returns>The 1 or 2 closest directions from <paramref name="loc1"/> to <paramref name="loc2"/></returns>
        public ICollection<Direction> GetDirections(Location loc1, Location loc2)
        {
            List<Direction> directions = new List<Direction>();

            if (loc1.Row < loc2.Row)
            {
                if (loc2.Row - loc1.Row >= Height / 2)
                    directions.Add(Direction.North);
                if (loc2.Row - loc1.Row <= Height / 2)
                    directions.Add(Direction.South);
            }
            if (loc2.Row < loc1.Row)
            {
                if (loc1.Row - loc2.Row >= Height / 2)
                    directions.Add(Direction.South);
                if (loc1.Row - loc2.Row <= Height / 2)
                    directions.Add(Direction.North);
            }

            if (loc1.Col < loc2.Col)
            {
                if (loc2.Col - loc1.Col >= Width / 2)
                    directions.Add(Direction.West);
                if (loc2.Col - loc1.Col <= Width / 2)
                    directions.Add(Direction.East);
            }
            if (loc2.Col < loc1.Col)
            {
                if (loc1.Col - loc2.Col >= Width / 2)
                    directions.Add(Direction.East);
                if (loc1.Col - loc2.Col <= Width / 2)
                    directions.Add(Direction.West);
            }

            return directions;
        }


        Dictionary<Location, int[,]> cachedRouteCalculations = new Dictionary<Location, int[,]>();
        public void ClearCache()
        {
            cachedRouteCalculations = new Dictionary<Location, int[,]>();
            visible = null;
        }

        public int GetDistance(Location start, Location end)
        {
            if (!cachedRouteCalculations.ContainsKey(end))
            {
                cachedRouteCalculations[end] = FillPathCalculation(end, int.MaxValue);
            }
            Perf.Start("GetDistance");

            var result = cachedRouteCalculations[end].At(start);
            //Log.Write("Distance from {0} to {1} is {2}", start, end, result);
            Perf.End("GetDistance");
            return result == 0 ? int.MaxValue : result;
        }

        public List<Location> BuildPath(Location start, Location end)
        {
            if (!cachedRouteCalculations.ContainsKey(end))
            {
                cachedRouteCalculations[end] = FillPathCalculation(end, int.MaxValue);
            }

            return BuildPath(cachedRouteCalculations[end], start, end);
        }

        public List<Location> BuildPath(Location start, Location end, HashSet<Location> ocupied)
        {
            if (!cachedRouteCalculations.ContainsKey(end))
            {
                cachedRouteCalculations[end] = FillPathCalculation(end, int.MaxValue);
            }

            foreach (var l in ocupied)
            {
                cachedRouteCalculations[end][l.Row, l.Col] = -1;
            }

            return BuildPath(cachedRouteCalculations[end], start, end);
        }

        public int[,] FillPathCalculation(Location end, int maxDepth)
        {
            Perf.Start("FillPathCalculation");
            int[,] calculation = new int[Height, Width];
            calculation.Iter((r, c, v) => calculation[r, c] =
                Env.Map[r, c] == Tile.Unseen || Env.Map[r, c] == Tile.Water
                ? -1 : 0);
            foreach (var f in Env.Food)
            {
                calculation[f.Row, f.Col] = -1;
            }
            calculation[end.Row, end.Col] = int.MaxValue;

            FillPathCalculation(new List<Location>() { end }, calculation, 1, maxDepth);

            calculation[end.Row, end.Col] = 0;
            Perf.End("FillPathCalculation");

            return calculation;
            //Log.LogPathCalculation(calculation);
        }

        private void FillPathCalculation(IEnumerable<Location> next, int[,] calculation, int depth, int maxDepth)
        {
            if (depth >= maxDepth)
                return;

            var toProcess = new List<Location>();

            foreach (var l in next)
            {
                foreach (var d in Engine.Aim.Values.Select(al => (l + al).Normalize()))
                {
                    if (calculation[d.Row, d.Col] != 0)
                        continue;

                    calculation[d.Row, d.Col] = depth;
                    toProcess.Add(d);
                }
            }

            if (!toProcess.Any())
                return;

            FillPathCalculation(toProcess, calculation, depth + 1, maxDepth);
        }

        private IEnumerable<Location> GetAims(int[,] calculation, Location l)
        {
            var next = Engine.Aim.Values.Select(a => (a + l).Normalize());
            return next.Where(loc => calculation.At(loc) == 0);
        }

        public List<Location> BuildPath(int[,] calculation, Location start, Location end)
        {
            var depth = calculation.At(start);
            var result = new List<Location>();
            while (depth > 0)
            {
                foreach (var ofs in Engine.Aim.Values)
                {
                    var t = (start + ofs).Normalize();
                    if (calculation.At(t) == -1)
                        continue;

                    if (calculation.At(t) < depth)
                    {
                        start = t;
                        depth--;
                        result.Add(t);
                        break;
                    }
                }
            }
            return result;
        }

        public List<Location> DirectionTo(Location start, Predicate<Location> p)
        {
            Perf.Start("DirectionTo");
            bool[,] visited = new bool[Height, Width];
            visited[start.Row, start.Col] = true;
            var result = DirectionTo(new List<Location>() { start }, (l, d) => p(l), (l, d) => GetIsPassable(l) && this[l] != Tile.Unseen, visited, 1);

            //if (!result.Any())
            //    Log.WriteVisited(visited, start);

            Perf.End("DirectionTo");
            return result;
        }

        private List<Location> DirectionTo(List<Location> startList,
            Func<Location, int, bool> resultPredicate, 
            Func<Location, int, bool> continuePredicate,
            bool[,] visited, int depth)
        {
            var toProcess = new List<Location>();

            foreach (var l in startList)
            {
                foreach (var d in Engine.Aim.Keys)
                {
                    var checkLocation = (l + Engine.Aim[d]).Normalize();
                    if (visited[checkLocation.Row, checkLocation.Col])
                        continue;
                    //if (this[checkLocation] == Tile.Water || this[checkLocation] == Tile.Unseen)
                    //    continue;

                    if (!continuePredicate(checkLocation, depth + 1))
                        continue;

                    visited[checkLocation.Row, checkLocation.Col] = true;

                    toProcess.Add(checkLocation);
                }
            }

            if (!toProcess.Any())
                return new List<Location>();

            foreach (var l in toProcess)
            {
                if (resultPredicate(l, depth))
                    return new List<Location>() { l };
            }

            var found = DirectionTo(toProcess, resultPredicate, continuePredicate, visited, depth + 1);

            if (!found.Any())
                return found;

            var from = toProcess.First(l => StupidGetDistance(l, found.First()) == 1);
            var result = new List<Location>() { from };
            result.AddRange(found);

            return result;
        }

        public void Iter(Action<int, int, Tile> a)
        {
            for (int row = 0; row < this.Height; row++)
                for (int col = 0; col < this.Width; col++)
                    a(row, col, this[row, col]);
        }

        bool[,] visible;
        public bool IsVisible(Location l)
        {
            if (visible == null)
            {
                visible = new bool[this.Height, this.Width];
                foreach (var a in Env.MyAnts)
                {
                    SetVisibleTiles(a, visible);
                }
            }
            return visible[l.Row, l.Col];
        }

        private void SetVisibleTiles(Ant a, bool[,] visible)
        {
            foreach (var v in Env.GetVisibilityOffsets())
            {
                var l = (a.Location + v).Normalize();
                visible[l.Row, l.Col] = true;
            }
        }

    }
}
